﻿using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System;
using VSLangProj;

namespace VsExt.FormatDocument
{
    /// <summary>
    /// This is the class that implements the package exposed by this assembly.
    ///
    /// The minimum requirement for a class to be considered a valid package for Visual Studio
    /// is to implement the IVsPackage interface and register itself with the shell.
    /// This package uses the helper classes defined inside the Managed Package Framework (MPF)
    /// to do it: it derives from the Package class that provides the implementation of the 
    /// IVsPackage interface and uses the registration attributes defined in the framework to 
    /// register itself and its components with the shell.
    /// </summary>
    // This attribute tells the PkgDef creation utility (CreatePkgDef.exe) that this class is
    // a package.
    // This attribute is used to register the information needed to show the this package
    // in the Help/About dialog of Visual Studio.
    // This attribute is needed to let the shell know that this package exposes some menus.
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [Guid(GuidList.guidFormatDocumentPkgString)]
    public sealed class VsExtFormatDocumentPackage : Package
    {
        /// <summary>
        /// Default constructor of the package.
        /// Inside this method you can place any initialization code that does not require 
        /// any Visual Studio service because at this point the package object is created but 
        /// not sited yet inside Visual Studio environment. The place to do all the other 
        /// initialization is the Initialize method.
        /// </summary>
        public VsExtFormatDocumentPackage()
        {
            Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering constructor for: {0}", this.ToString()));
        }

        /// <summary>
        /// Gets or sets the command to execute.
        /// </summary>
        /// <value>The command to execute.</value>
        private string CommandToExecute { get; set; }

        /// <summary>
        /// Gets or sets the status bar finished text.
        /// </summary>
        /// <value>The status bar finished text.</value>
        private string StatusBarFinishedText { get; set; }

        /// <summary>
        /// Gets or sets the status bar starting text.
        /// </summary>
        /// <value>The status bar starting text.</value>
        private string StatusBarStartingText { get; set; }

        /// <summary>
        /// Initialization of the package; this method is called right after the package is sited, so this is the place
        /// where you can put all the initialization code that rely on services provided by VisualStudio.
        /// </summary>
        protected override void Initialize()
        {
            Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));

            base.Initialize();

            this.CommandToExecute = "Edit.FormatDocument";

            this.StatusBarStartingText = "Format Document has started";

            this.StatusBarFinishedText = "Format Document is complete";

            // Add our command handlers for menu (commands must exist in the .vsct file)
            OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;

            if (null != mcs)
            {
                // Create the command for the menu item.
                CommandID menuCommandSolution = new CommandID(GuidList.guidFormatDocumentCmdSet, (int)PkgCmdIDList.cmdidFormatDocument);
                MenuCommand menuItemSolution = new MenuCommand(MenuItemCallback, menuCommandSolution);
                mcs.AddCommand(menuItemSolution);

                CommandID menuCommandProject = new CommandID(GuidList.guidFormatDocumentCmdSetProject, (int)PkgCmdIDList.cmdidFormatDocumentProject);
                MenuCommand menuItemProject = new MenuCommand(MenuItemCallback, menuCommandProject);
                mcs.AddCommand(menuItemProject);

                CommandID menuCommandCodeWindow = new CommandID(GuidList.guidFormatDocumentCmdSetCodeWindow, (int)PkgCmdIDList.cmdidFormatDocumentCodeWindow);
                MenuCommand menuItemCodeWindow = new MenuCommand(MenuItemCallback, menuCommandCodeWindow);
                mcs.AddCommand(menuItemCodeWindow);

                CommandID menuCommandItemNode = new CommandID(GuidList.guidFormatDocumentCmdSetItemNode, (int)PkgCmdIDList.cmdidFormatDocumentItemNode);
                MenuCommand menuItemItemNode = new MenuCommand(MenuItemCallback, menuCommandItemNode);
                mcs.AddCommand(menuItemItemNode);
            }
        }

        private void ExecuteCommand()
        {
            DTE2 dte = (DTE2)GetService(typeof(DTE));

            dte.StatusBar.Text = this.StatusBarStartingText;

            // Get the the Solution Explorer tree
            UIHierarchy solutionExplorer = dte.ToolWindows.SolutionExplorer;

            // Check if there is any open solution
            if ((solutionExplorer.UIHierarchyItems.Count == 0))
            {
                return;
            }

            UIHierarchyItem item = GetSelectedUIHierarchy(solutionExplorer);

            // is the selected item expanded or collapsed
            bool isExpanded = item.UIHierarchyItems.Expanded;

            // are we working with a single code file
            bool isCodeFileSelected = item.UIHierarchyItems.Count == 0 ? true : false;

            // as long as a single item was selected then continue.
            if (item != null)
            {
                item.DTE.SuppressUI = true;

                // process each item in the hierarchy.
                ProcessItems(item);

                // keep the originally selected item selected.
                item.Select(vsUISelectionType.vsUISelectionTypeSelect);

                if (!isCodeFileSelected && !isExpanded)
                {
                    // this will collapse the solution/project node.
                    // A project node will collapse even if it's in a solution folder.
                    solutionExplorer.DoDefaultAction();
                }

                item.DTE.SuppressUI = false;
            }

            dte.StatusBar.Text = this.StatusBarFinishedText;
        }

        /// <summary>
        /// Gets the selected UI hierarchy.
        /// </summary>
        /// <returns></returns>
        private UIHierarchyItem GetSelectedUIHierarchy(UIHierarchy solutionExplorer)
        {
            object[] selection = solutionExplorer.SelectedItems as object[];

            if (selection != null && selection.Length == 1)
            {
                return selection[0] as UIHierarchyItem;
            }

            return null;
        }

        /// <summary>
        /// This function is the callback used to execute a command when the a menu item is clicked.
        /// See the Initialize method to see how the menu item is associated to this function using
        /// the OleMenuCommandService service and the MenuCommand class.
        /// </summary>
        private void MenuItemCallback(object sender, EventArgs e)
        {
            ExecuteCommand();
        }

        /// <summary>
        /// Executes the Command against the code file.
        /// </summary>
        /// <param name="item">The item.</param>
        private void ProcessCodeFile(ProjectItem item)
        {
            Window window = null;

            bool allreadyOpen = item.IsOpen;

            if (!allreadyOpen)
            {
                window = item.Open(EnvDTE.Constants.vsViewKindCode);
            }

            try
            {
                item.Document.Activate();

                item.DTE.ExecuteCommand(CommandToExecute, string.Empty);

                if (!allreadyOpen && window != null)
                {
                    window.Close(vsSaveChanges.vsSaveChangesYes);
                }
                else
                {
                    item.Document.Save();
                }
            }
            catch (COMException)
            {
                // maybe implement logging in the future.
                // but we'll need a way to allow the user to turn it off.
            }
        }

        /// <summary>
        /// Processes each item in the UIHierarchy.
        /// </summary>
        /// <param name="item">The item.</param>
        private void ProcessItems(UIHierarchyItem item)
        {
            // any document that is open will cause it's parent node to expand.
            // we want to return solution explorer to the same state it was in
            // before the command was executed.
            bool isExpanded = item.UIHierarchyItems.Expanded;

            int hierarchyCount = item.UIHierarchyItems.Count;

            if (hierarchyCount == 0)
            {
                ProjectItem projectItem = item.Object is ProjectItem ? (ProjectItem)item.Object : null;

                // we want to execute the command against code files only
                if (projectItem != null && projectItem.ContainingProject.Kind == PrjKind.prjKindCSharpProject && projectItem.FileCodeModel != null)
                {
                    ProcessCodeFile(projectItem);
                }
            }
            else
            {
                foreach (UIHierarchyItem nestedItem in item.UIHierarchyItems)
                {
                    ProcessItems(nestedItem);
                }
            }

            item.UIHierarchyItems.Expanded = isExpanded;
        }
    }
}
